//
//  RSDBDatabase.m
//  Cellar Tracker
//
//  Created by Johnnie Walker on 09/07/2008.
//  Copyright 2008 Random Sequence. All rights reserved.
//

#import "RSDBDatabase.h"
#import "RSDBResultSet.h"

@implementation RSDBDatabase
@synthesize databaseURL;

+ (id)databaseWithPath:(NSString*)aPath {
    return [[[[self class] alloc] initWithPath:aPath] autorelease];
}

- (id)initWithPath:(NSString*)aPath {
    self = [super initWithPath:aPath];

	[self setDatabaseURL:[NSURL fileURLWithPath:aPath]];
	[self setShouldCacheStatements:YES];
	
	return self;
}

- (BOOL) beginTransaction {
	if ([self inTransaction]) {
		return NO;
	}
	return [super beginTransaction]; 
}

#pragma mark - Introspection

-(RSDBResultSet *)tables
{	
	return [self executeQuery:@"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"];
}

-(RSDBResultSet *)schemaForTable:(NSString *)tableName
{
	NSString *query = [NSString stringWithFormat:@"PRAGMA table_info(%@)",tableName];
	//	NSLog(@"query: %@",query);
	RSDBResultSet *schema = [self executeQuery:query];	
	
	if ([schema columnCount] == 0) {
		[schema close];
		return nil;
	}
	
	return schema; 
}

- (void)setUserVersion:(int)userVersion
{
	[self executeUpdate:[NSString stringWithFormat:@"PRAGMA user_version = %i",userVersion]];	
}

-(int)userVersion
{
	NSString *query = [NSString stringWithFormat:@"PRAGMA user_version"];
	RSDBResultSet *results = [self executeQuery:query];	
	
	int userVersion = -1;
	
	if ([results next]) {
		userVersion = [results intForColumnIndex:0];
	}
	
	[results close];
	return userVersion;	
}

- (BOOL)tableExists:(NSString *)tableName
{
	RSDBResultSet *results = [self executeQuery:@"SELECT name FROM sqlite_master WHERE type = \"table\" AND name = ?" arguments:[NSArray arrayWithObject:tableName]];
	BOOL exists = [results next];
	[results close];
	return exists;
}

#pragma mark -
#pragma mark Queries

/*
 
 These methods are commented out in the superclass because you can't call a function with a va_args list programatically. 

 */

- (FMStatement *)statementFromSQL:(NSString *)sql
{
    int rc                  = 0x00;	
    sqlite3_stmt *pStmt     = 0x00;
    FMStatement *statement  = 0x00;	
	
    if (shouldCacheStatements) {
        statement = [self cachedStatementForQuery:sql];
        pStmt = statement ? [statement statement] : 0x00;
    }
    
    int numberOfRetries = 0;
    BOOL retry          = NO;
    
    if (!pStmt) {
        do {
            retry   = NO;
            rc      = sqlite3_prepare(db, [sql UTF8String], -1, &pStmt, 0);
            
            if (SQLITE_BUSY == rc) {
                retry = YES;
                usleep(20);
                
                if (busyRetryTimeout && (numberOfRetries++ > busyRetryTimeout)) {
                    NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [self databasePath]);
                    NSLog(@"Database busy");
                    sqlite3_finalize(pStmt);
                    [self setInUse:NO];
                    return nil;
                }
            }
            else if (SQLITE_OK != rc) {
                
                rc = sqlite3_finalize(pStmt);
                
                if (logsErrors) {
                    NSLog(@"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]);
                    NSLog(@"DB Query: %@", sql);
                    if (crashOnErrors) {
#ifdef __BIG_ENDIAN__
                        asm{ trap };
#endif
                        *(long*)0 = 0xDEADBEEF;
                    }
                }
                
                [self setInUse:NO];
                return nil;
            }
        }
        while (retry);
    }
	
    if (!statement) {
        statement = [[[FMStatement alloc] init] autorelease];
        [statement setStatement:pStmt];
        
        if (shouldCacheStatements) {
            [self setCachedStatement:statement forQuery:sql];
        }
    }	
	
	return statement;
}

- (id) executeQuery:(NSString*)sql, ... {
    va_list args;
    va_start(args, sql);
    
	//    id result = [self executeQuery:sql arguments:args];
	FMStatement *statement  = [self statementFromSQL:sql];	
    int argsCount = sqlite3_bind_parameter_count([statement statement]);	
	NSMutableArray *argsArray = [NSMutableArray arrayWithCapacity:argsCount];
	id arg;	
	unsigned i;
	for	(i=0; i<argsCount; i++) {
		arg = va_arg(args, id);
        if (!arg) {
            break;
        }						
		[argsArray addObject:arg];
	}
    id result = [self executeQuery:sql arguments:argsArray];	
	
    va_end(args);
    return result;
}

- (id) executeQuery:(NSString *)sql arguments:(NSArray *)args {
    
    if (inUse) {
        [self compainAboutInUse];
        return nil;
    }
    
    [self setInUse:YES];
    
    RSDBResultSet *rs = nil;
    
    FMStatement *statement  = [self statementFromSQL:sql];
    sqlite3_stmt *pStmt     = [statement statement];
    
    if (traceExecution && sql) {
        NSLog(@"%@ executeQuery: %@", self, sql);
    }
        
    id obj;
    int idx = 0;
    int queryCount = sqlite3_bind_parameter_count(pStmt); // pointed out by Dominic Yu (thanks!)
    
    NSEnumerator *argsEnu = [args objectEnumerator];
	
    while (idx < queryCount) {
        obj = [argsEnu nextObject];
        
        if (!obj) {
            break;
        }
        
        if (traceExecution) {
            NSLog(@"obj: %@", obj);
        }
        
        idx++;
        
        [self bindObject:obj toColumn:idx inStatement:pStmt];
    }
    
    if (idx != queryCount) {
        NSLog(@"Error: the bind count is not correct for the # of variables (executeQuery)");
        sqlite3_finalize(pStmt);
        [self setInUse:NO];
        return nil;
    }
    
    [statement retain]; // to balance the release below
    
    if (!statement) {
        statement = [[FMStatement alloc] init];
        [statement setStatement:pStmt];
        
        if (shouldCacheStatements) {
            [self setCachedStatement:statement forQuery:sql];
        }
    }
    
    // the statement gets close in rs's dealloc or [rs close];
    rs = [RSDBResultSet resultSetWithStatement:statement usingParentDatabase:self];
    [rs setQuery:sql];
    
    statement.useCount = statement.useCount + 1;
    
    [statement release];    
    
    return rs;
}

- (BOOL) executeUpdate:(NSString*)sql arguments:(NSArray *)args {
    
    if (inUse) {
        [self compainAboutInUse];
        return NO;
    }
    
    [self setInUse:YES];
    
    int rc                   = 0x00;
    FMStatement *cachedStmt  = [self statementFromSQL:sql];
    sqlite3_stmt *pStmt     = [cachedStmt statement];
    
    if (traceExecution && sql) {
        NSLog(@"%@ executeUpdate: %@", self, sql);
    }	
    
    id obj;
    int idx = 0;
    int queryCount = sqlite3_bind_parameter_count(pStmt);

	NSEnumerator *argsEnu = [args objectEnumerator];
	
    while (idx < queryCount) {
        
        obj = [argsEnu nextObject];
        
        if (!obj) {
            break;
        }
		
        if (traceExecution) {
            NSLog(@"obj: %@", obj);
        }
        
        idx++;
        
        [self bindObject:obj toColumn:idx inStatement:pStmt];
    }
    
    if (idx != queryCount) {
        NSLog(@"Error: the bind count is not correct for the # of variables (%@) (executeUpdate)", sql);
        sqlite3_finalize(pStmt);
        [self setInUse:NO];
        return NO;
    }
    
    /* Call sqlite3_step() to run the virtual machine. Since the SQL being
     ** executed is not a SELECT statement, we assume no data will be returned.
     */
	
    int numberOfRetries = 0;
    BOOL retry          = NO;	
    numberOfRetries = 0;
    do {
        rc      = sqlite3_step(pStmt);
        retry   = NO;
        
        if (SQLITE_BUSY == rc) {
            // this will happen if the db is locked, like if we are doing an update or insert.
            // in that case, retry the step... and maybe wait just 10 milliseconds.
            retry = YES;
            usleep(20);
            
            if (busyRetryTimeout && (numberOfRetries++ > busyRetryTimeout)) {
                NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [self databasePath]);
                NSLog(@"Database busy");
                retry = NO;
            }
        }
        else if (SQLITE_DONE == rc || SQLITE_ROW == rc) {
            // all is well, let's return.
        }
        else if (SQLITE_ERROR == rc) {
            NSLog(@"Error calling sqlite3_step (%d: %s) eu", rc, sqlite3_errmsg(db));
            NSLog(@"DB Query: %@", sql);
        }
        else if (SQLITE_MISUSE == rc) {
            // uh oh.
            NSLog(@"Error calling sqlite3_step (%d: %s) eu", rc, sqlite3_errmsg(db));
            NSLog(@"DB Query: %@", sql);
        }
        else {
            // wtf?
            NSLog(@"Unknown error calling sqlite3_step (%d: %s) eu", rc, sqlite3_errmsg(db));
            NSLog(@"DB Query: %@", sql);
        }
        
    } while (retry);
    
    assert( rc!=SQLITE_ROW );
    
    
    if (shouldCacheStatements && !cachedStmt) {
        cachedStmt = [[FMStatement alloc] init];
        
        [cachedStmt setStatement:pStmt];
        
        [self setCachedStatement:cachedStmt forQuery:sql];
        
        [cachedStmt release];
    }
    
    if (cachedStmt) {
        cachedStmt.useCount = cachedStmt.useCount + 1;
        rc = sqlite3_reset(pStmt);
    }
    else {
        /* Finalize the virtual machine. This releases all memory and other
         ** resources allocated by the sqlite3_prepare() call above.
         */
        rc = sqlite3_finalize(pStmt);
    }
    
	[self clearCachedStatements];
    [self setInUse:NO];
    
    return (rc == SQLITE_OK);
}

- (BOOL) executeUpdate:(NSString*)sql, ... {
    va_list args;
    va_start(args, sql);
    
	FMStatement *statement  = [self statementFromSQL:sql];	
    int argsCount = sqlite3_bind_parameter_count([statement statement]);	
	NSMutableArray *argsArray = [NSMutableArray arrayWithCapacity:argsCount];
	id arg;	
	unsigned i;
	for	(i=0; i<argsCount; i++) {
		arg = va_arg(args, id);
        if (!arg) {
            break;
        }						
		[argsArray addObject:arg];
	}	
    BOOL result = [self executeUpdate:sql arguments:argsArray];
    
    va_end(args);
    return result;
}

@end
